Vamos a trabajar con datos tabulares. Existen 3 reglas que logran que un conjunto de datos tabulares esté “limpio”:
Para todos los conjuntos de datos con los que trabajemos es importante tener un contexto que nos brinde información sobre cómo fueron recolectados, en qué año(s), qué técnicas de muestreo y recolección se usaron para obtener las observaciones y qué personas o entidades fueron los responsables.
Ejemplo:
Para profundizar: revise el material sobre cómo limpiar tablas.
En esta práctica vamos a trabajar con la base de datos Boston Housing, y podemos consultar el contexto de dichos datos haciendo clic en este tunel secreto.
¿Qué aprenderemos?
Carguemos los datos:
# Cargamos los paquetes
library("tidyverse")
library("readxl")
# Leemos los datos desde un archivo de Excel
read_xlsx(
path = "data/Boston_Housing.xlsx",
sheet="Data"
) -> boston_housing_xlsx
str(boston_housing_xlsx)
# Convierto variables respectivas a factores
# Esto es muy importante para procesos de limpieza de datos
factores <- c("CHAS")
boston_housing_xlsx %>% mutate_at(factores,factor) -> boston_housing_xlsxEn cualquier escenario, es posible que tengamos datos faltantes. Veámos cómo abordar esta situación.
Los datos faltantes (missing data) son un problema frecuente en todos los tipos de estudios y análisis, sin importar que el diseño sea muy estricto o que los investigadores/analistas traten de prevenirlo.
En ciencia de datos podemos realizar un proceso de imputación de datos, que consiste en asignar un valor a un ítem para el que previamente no se tenia información.
Existen numerosos métodos de imputación de datos, entre otros:
Vamos a retirar algunos datos de la base de forma aleatoria.
# Instalamos el paquete mice
# install.packages("mice")
# Cargamos mice
library("mice")
# Hacemos una copia de la base de datos en otro objeto llamado datos_completos
datos_completos <- boston_housing_xlsx
# "amputamos" datos usando el método MCAR: missing completely at random
ampute(datos_completos, prop = 0.5, mech = "MCAR", run = TRUE)$amp -> datos_incompletos
# Mapeamos el número de NAs por cada columna
datos_incompletos %>% map_df(is.na) %>% colSums()## CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX
## 19 15 14 9 19 20 17 20 23 17
## PTRATIO B LSTAT MEDV
## 22 20 20 24
Ahora que tenemos datos perdidos en la base de datos, algo que sucede con (mucha) frecuencia, debemos examinar el “comportamiento” de la pérdida de datos, esto es, identificar si existen patrones o situaciones que nos den indicio de por qué se perdieron los datos.
Podemos tener tres patrones de pérdida de datos:
Es deseable que si tenemos datos perdidos esto se deba al azar, y en ese caso, podríamos proceder a imputar los datos faltantes en las covariables (no es tan deseable en la variable objetivo/feature). Si tenemos patrones irregulares de pérdida de datos, sería un indicio de que podemos tener problemas en la captura, sistematización, almacenamiento o distribución de los datos.
Podemos hacer distintas visualizaciones para entender el comportamiento de los datos perdidos.
# Instalamos los paquetes naniar y VIM
# install.packages("naniar")
# install.packages("VIM")
# Cargamos los paquetes
library("naniar")
library("VIM")
# Visualización de patrones con naniar
# Graficamos en orden descendente las variables con más datos perdidos
# Nos muestra también si hay relaciones de pérdida de datos entre distintas variables
gg_miss_upset(datos_incompletos)# Visualización de patrones con VIM
# Graficamos la proporción de datos incompletos por variable
# Nos muestra también si hay relaciones de pérdida de datos entre distintas variables
aggr(datos_incompletos,numbers=T,sortVar=T)##
## Variables sorted by number of missings:
## Variable Count
## MEDV 0.04743083
## RAD 0.04545455
## PTRATIO 0.04347826
## RM 0.03952569
## DIS 0.03952569
## B 0.03952569
## LSTAT 0.03952569
## CRIM 0.03754941
## NOX 0.03754941
## AGE 0.03359684
## TAX 0.03359684
## ZN 0.02964427
## INDUS 0.02766798
## CHAS 0.01778656
# Visualización de patrones con mice
# Se enfoca en mostrar si hay relaciones de pérdida de datos entre distintas variables
md.pattern(datos_incompletos, plot = TRUE, rotate.names = TRUE)## CHAS INDUS ZN AGE TAX CRIM NOX RM DIS B LSTAT PTRATIO RAD MEDV
## 247 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
## 24 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1
## 23 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1
## 22 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1
## 20 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1
## 20 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1
## 20 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1
## 20 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1
## 19 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1
## 19 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1
## 17 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1
## 17 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1
## 15 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1
## 14 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1
## 9 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 9 14 15 17 17 19 19 20 20 20 20 22 23 24 259
Advertencia: este tipo de análisis tienen sentido si por la naturaleza del problema esperamos tener todos los datos completos. Si hay columnas en donde es esperable tener datos faltantes (por ejemplo, por respuestas opcionales o variables que parten una muestra) deberíamos realizar la gráfica anterior solamente con las columnas (variables) de las que esperamos datos completos.
Habiendo comprobado que en nuestros datos los valores perdidos se deben al azar y no superan umbrales de trabajo en ciencia de datos, podemos ahora así aplicar métodos de imputación de datos.
# Filtramos únicamente variables numéricas
datos_incompletos_num <- Filter(is.numeric, datos_incompletos)
# Llamamos el comando mice para imputar los datos
# Asignamos unos parámetros de imputación
mice(datos_incompletos_num,
m = 1, # Número de imputaciones múltiples
maxit = 1, # Número de iteraciones
method = "mean", # Método de imputación
printFlag = FALSE) %>%
mice::complete() -> base_datos_imputados_promedios_mice
# Verificamos que efectivamente ya no hayan datos faltantes
base_datos_imputados_promedios_mice %>% map_df(is.na) %>% colSums()## CRIM ZN INDUS NOX RM AGE DIS RAD TAX PTRATIO
## 0 0 0 0 0 0 0 0 0 0
## B LSTAT MEDV
## 0 0 0
# Llamamos el comando hotdeck
hotdeck(datos_incompletos) -> datos_imputados_hd
# Verificamos que efectivamente ya no hayan datos faltantes
datos_imputados_hd[,c(1:ncol(datos_incompletos))] %>% map_df(is.na) %>% colSums()## CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX
## 0 0 0 0 0 0 0 0 0 0
## PTRATIO B LSTAT MEDV
## 0 0 0 0
# Llamamos el comando mice para imputar los datos
mice(datos_incompletos, printFlag = FALSE) %>%
mice::complete() -> datos_imputados_mice
# Verificamos que efectivamente ya no hayan datos faltantes
datos_imputados_mice %>% map_df(is.na) %>% colSums()## CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX
## 0 0 0 0 0 0 0 0 0 0
## PTRATIO B LSTAT MEDV
## 0 0 0 0
En nuestros análisis debemos examinar la presencia de datos atípicos, en la medida que pueden afectar los resultados de las estimaciones, modelos y pruebas de hipótesis.
Para detectar datos atípicos podemos seguir dos caminos:
Veamos dos ejemplos usando la base de datos de Boston Housing.
Primero, una detección de atípicos en una variable en específico (MEDV que es una variable objetivo).
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 5.00 17.02 21.20 22.53 25.00 50.00
# Boxplot de una variable
boxplot(boston_housing_xlsx$MEDV)
# Valores de potenciales outliers
boxplot.stats(boston_housing_xlsx$MEDV)$out## [1] 38.7 43.8 41.3 50.0 50.0 50.0 50.0 37.2 39.8 37.9 50.0 50.0 42.3 48.5 50.0
## [16] 44.8 50.0 37.6 46.7 41.7 48.3 42.8 44.0 50.0 43.1 48.8 50.0 43.5 45.4 46.0
## [31] 50.0 37.3 50.0 50.0 50.0 50.0 50.0
# Filas donde se ubican las observaciones atípicas
outliers <- boxplot(boston_housing_xlsx$MEDV)$out## [1] 98 99 158 162 163 164 167 180 181 183 187 196 203 204 205 225 226 227 229
## [20] 233 234 254 257 258 262 263 268 269 281 283 284 292 369 370 371 372 373
Segundo, una detección de atípicos basado en un análisis multivariado por medio de componentes principales (hay otras técnicas más, por ejemplo, la distancia de Cook).
# Instalamos paquetes
# install.packages("FactoMineR")
# install.packages("factoextra")
# install.packages("ggpubr")
# install.packages("magrittr")
# Cargamos paquetes
library("FactoMineR")
library("factoextra")
library("ggpubr")
library("magrittr")
# Estandarizamos las variables
datos_numericos <- Filter(is.numeric, datos_completos)
datos_numericos %>%
mutate_all(scale) -> data_estandarizada
# Ajusto componentes principales usando el método PCA()
acp = PCA(data_estandarizada, graph=F)Con el siguiente código calculamos la distancia al vecino más cercano.
data_estandarizada %>%
dist %>% as.matrix() %>% add(diag(Inf, ncol(.))) %>%
apply(1, min) %>% enframe() %>% arrange(desc(value))## # A tibble: 506 × 2
## name value
## <chr> <dbl>
## 1 381 3.24
## 2 406 3.24
## 3 419 3.17
## 4 415 3.02
## 5 368 2.92
## 6 365 2.73
## 7 103 2.61
## 8 254 2.35
## 9 366 2.34
## 10 266 2.30
## # ℹ 496 more rows
Por medio de diversos procedimientos estadísticos podemos detectar datos atípicos. ¿Qué hacer con ellos? Depende. Hay que sopesar el contexto de los datos, la naturaleza del problema y de cada variable, así como combinar elementos de juicio estadístico como elementos de juicio profesionales de otras áreas.
Recuerde que en programación los nombres importan (naming). Siguiendo ese marco de referencia, asegúrese de que las variables (columnas):
## [1] 14
## [1] "CRIM" "ZN" "INDUS" "CHAS" "NOX" "RM" "AGE"
## [8] "DIS" "RAD" "TAX" "PTRATIO" "B" "LSTAT" "MEDV"
# Podríamos declarar un vector con los nuevos nombres que necesitemos
# nombres_adecuados <- c(
# "nombre_variable_1",
# "nombre_variable_2",
# ...,
# )
# Y luego asignarlos a nuestra base de datos
# nombres_adecuados -> names(datos_completos)
# Otro ejemplo
# Si tuviésemos variables con espacios, podríamos reemplazar todos los espacios así
# names(datos_completos) <- str_replace_all(names(datos_completos), c(" " = "_"))Podemos buscar y eliminar duplicados basados en un columna, por ejemplo, cuando esperamos tener datos únicos de un individuo y tenemos una columna para identificarlo.
## Ejemplo: acá dejamos valores únicos en la columna MEDV
distinct(datos_completos, MEDV, .keep_all = TRUE) -> datos_completos_MEDV_unico
## Ejemplo: acá dejamos valores únicos en la columna DIS
distinct(datos_completos, DIS, .keep_all = TRUE) -> datos_completos_DIS_unicoTambién podemos buscar y eliminar duplicados basados en toda la fila.
En algunos problemas, conviene convertir variables que son continuas en variables agrupadas por intervalos. Este proceso se llama discretización. Veamos un ejemplo creando un rango de edad para las viviendas de la base de datos.
# Instalamos paquete lubridate
# install.packages("lubridate")
# Cargamos paquete lubridate
library("lubridate")
# Discretizamos la edad (AGE) de los datos del censo
# mutate() anexa/crea variables a la base de datos
datos_completos %>% mutate(
edad_hoy = datos_completos$AGE+50,
rango_edad = cut(edad_hoy, c(0, 50, 75, 90, 105, 120, 135, Inf))
) -> datos_completos_fechas
head(datos_completos_fechas[(ncol(datos_completos_fechas)-1):ncol(datos_completos_fechas)])## # A tibble: 6 × 2
## edad_hoy rango_edad
## <dbl> <fct>
## 1 115. (105,120]
## 2 129. (120,135]
## 3 111. (105,120]
## 4 95.8 (90,105]
## 5 104. (90,105]
## 6 109. (105,120]
Este mismo principio podría servir para crear rangos etarios en poblaciones, niveles de ingresos, niveles de pobreza, etc.